#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Base Library for VMWare"""
import atexit
import os
from os.path import join
from util import file_exists
import configparser
import ssl
import tempfile
import urllib
import zipfile
from contextlib import closing
from vsphere.models import DeployParam
from vm_monitoring.settings import BASE_CONF_DIR

import requests

try:
    from pyVim import connect
except ImportError:  # This will occur because macOS is case insencitive.
    # Actually pyvim is pyVim here, not the real pyvim module.
    from pyvim import connect
try:
    from pyVmomi import vim
    from pyVmomi import vmodl
except ImportError:
    raise RuntimeError('Failed to import VMWare libraries for'
                       'python. %s' % ImportError.message)
_NBVA_TAG = 'The NetBackup Virtual Appliance is a combined master'
_DATASTORES = [
    'SC8000_LUN03_ALL_Saipu Liu',
    'SC8000_LUN12_WAN_TW',
    'SC8000_LUN10_Code Insurance']


class VM(object):
    """ VM """
    def _trust_vcenter_cert(self):
        certUrl = 'http://' + self.vcenter + '/certs/download'
        # Download root certificate from vcenter
        print "Downloading VMCA certificate from " + certUrl
        try:
            certPkg, certPkgHeaders = urllib.urlretrieve(certUrl)
        except:
            print "Download of VMCA certificate failed. vCenter " \
                + self.vcenter + " probably doesn't have an embedded CA"
            rget = requests.get
            requests.get = lambda obj, *args, **kwargs: rget(obj, verify=False)
            return
        # Extract the certificate file
        certFile = tempfile.NamedTemporaryFile(delete=False)
        with closing(zipfile.ZipFile(certPkg)) as zfile:
            for info in zfile.infolist():
                if info.filename.endswith('.0'):
                    print "Saving " + info.filename + " as " + certFile.name
                    certData = zfile.read(info)
                    certFile.write(certData)
                    certFile.close()
        # Trust the VMCA certificate
        os.environ["REQUESTS_CA_BUNDLE"] = certFile.name

    def _connect_to_vsphere(self):
        ssl_context = None
        if hasattr(ssl, '_create_unverified_context'):
            ssl_context = ssl._create_unverified_context()
        else:
            self._trust_vcenter_cert()

        print "Trying to connect to vSphere host:{0}".format(self.vcenter)
        service_instance = connect.SmartConnect(host=self.vcenter,
                                                user=self.vcenter_user,
                                                pwd=self.vcenter_password,
                                                port=443,
                                                sslContext=ssl_context)
        if not service_instance:
            raise Exception('Could not create service instance')
        atexit.register(connect.Disconnect, service_instance)
        print "Successfully connected to vSphere host:{0}".format(self.vcenter)
        return service_instance

    def _get_service_instance(self):
        try:
            self.service_instance.CurrentTime()
        except:
            # Try to reconnect if the connection was broken
            self.service_instance = self._connect_to_vsphere()
        return self.service_instance

    def _wait_for_tasks(self, tasks):
        service_instance = self._get_service_instance()
        property_collector = service_instance.content.propertyCollector
        task_list = [str(task) for task in tasks]
        # Create filter
        obj_specs = [vmodl.query.PropertyCollector.ObjectSpec(obj=task)
                     for task in tasks]
        property_spec = vmodl.query.PropertyCollector.PropertySpec(
            type=vim.Task, pathSet=[], all=True)
        filter_spec = vmodl.query.PropertyCollector.FilterSpec()
        filter_spec.objectSet = obj_specs
        filter_spec.propSet = [property_spec]
        pcfilter = property_collector.CreateFilter(filter_spec, True)
        try:
            version, state = None, None
            # Loop looking for updates till the state
            # moves to a completed state.
            while len(task_list):
                update = property_collector.WaitForUpdates(version)
                for filter_set in update.filterSet:
                    for obj_set in filter_set.objectSet:
                        task = obj_set.obj
                        for change in obj_set.changeSet:
                            if change.name == 'info':
                                state = change.val.state
                            elif change.name == 'info.state':
                                state = change.val
                            else:
                                continue

                            if not str(task) in task_list:
                                continue

                            if state == vim.TaskInfo.State.success:
                                # Remove task from taskList
                                task_list.remove(str(task))
                            elif state == vim.TaskInfo.State.error:
                                raise task.info.error
                # Move to next version
                version = update.version
        finally:
            if pcfilter:
                pcfilter.Destroy()

    def __init__(self, vcenter, user, password):
        self.vcenter = vcenter
        self.vcenter_user = user
        self.vcenter_password = password
        self.vms = []
        self._all_vms()
        self.dss = []
        self._all_dss()

    def _get_vm_by_uuid(self, uuid):
        content = self._get_service_instance().content
        vm = content.searchIndex.FindByUuid(None, uuid, True, False)
        if vm is None:
            raise Exception('Unable to locate vm with id "%s"' % uuid)
        else:
            print 'Found vm %s (%s)' % (vm.name, uuid)
            return vm

    def delete_vm_by_uuid(self, uuid):
        vm = self._get_vm_by_uuid(uuid)
        if vm.runtime.powerState == "poweredOn":
            print "Attempting to power off VM %s" % vm.name
            task = vm.PowerOffVM_Task()
            self._wait_for_tasks([task])

        print "Attempting to delete VM %s" % vm.name
        task = vm.Destroy_Task()
        self._wait_for_tasks([task])

    def poweroff_vm_by_uuid(self, uuid):
        vm = self._get_vm_by_uuid(uuid)
        if vm.runtime.powerState == "poweredOn":
            print "Attempting to power off VM %s" % vm.name
            task = vm.PowerOffVM_Task()
            self._wait_for_tasks([task])

    def change_power_status_by_uuid(self, uuid, action):
        func = None
        if action.upper() == 'POWERON':
            func = 'PowerOnVM_Task'
        elif action.upper() == 'POWEROFF':
            func = 'PowerOffVM_Task'
        else:
            raise Exception('Do not support action "%s"' % action)

        if not hasattr(uuid, '__iter__'):
            uuid = uuid,

        tasks = []
        for u in uuid:
            vm = self._get_vm_by_uuid(u)
            if vm.runtime.powerState[7:].upper() != action[5:].upper():
                if hasattr(vm, func):
                    tasks.append(getattr(vm, func)())

        if len(tasks):
            print "Attempting to %s VMs %s" % (action, str(uuid))
            self._wait_for_tasks(tasks)

    def _all_objs(self):
        objs = []
        content = self._get_service_instance().content
        container = content.viewManager.CreateContainerView(
            content.rootFolder, [vim.VirtualMachine], True)

        for c in container.view:
            try:
                if c.config.annotation.find(_NBVA_TAG) == 0:
                    objs.append(c)
            except:
                continue

        return objs

    def _all_dss(self):
        content = self._get_service_instance().content
        container = content.viewManager.CreateContainerView(
            content.rootFolder, [vim.Datastore], True)

        self.dss = []
        for tmp in container.view:
            if tmp.summary.name in _DATASTORES:
                self.dss.append(tmp)

    def _all_vms(self):
        objs = self._all_objs()
        for obj in objs:
            vm = {}
            vm['uuid'] = obj.config.uuid

            # TODO: try ...
            vm['name'] = obj.config.name
            vm['numCPU'] = obj.config.hardware.numCPU
            vm['memoryMB'] = obj.config.hardware.memoryMB
            vm['state'] = obj.runtime.powerState[7:]

            if obj.guest is not None and obj.guest.ipAddress is not None:
                vm['ip'] = obj.guest.ipAddress
            else:
                vm['ip'] = '-'

            if obj.guest is not None and obj.guest.hostName is not None:
                vm['hostname'] = obj.guest.hostName
            else:
                vm['hostname'] = '-'

            if obj.config.vAppConfig is not None and \
                    hasattr(obj.config.vAppConfig, 'product'):
                for _product in obj.config.vAppConfig.product:
                    if hasattr(_product, 'version'):
                        vm['version'] = _product.version
                        break
                else:
                    vm['version'] = '-'

            vm['base_disks'] = []
            total_capacity_in_kb = 0L
            total_thin_capacity_in_kb = 0L
            total_thick_capacity_in_kb = 0L
            for dev in obj.config.hardware.device:
                if isinstance(dev, vim.vm.device.VirtualDisk):
                    if dev.key != 2000:
                        _disk = {}
                        _disk['label'] = dev.deviceInfo.label
                        _disk['uuid'] = dev.backing.uuid
                        _disk['diskMode'] = dev.backing.diskMode
                        _disk['key'] = dev.key
                        _disk['size'] = '%.2f TB' % (
                                dev.capacityInKB / 2.0 ** (10 * 3))
                        _disk['capacityInKB'] = dev.capacityInKB
                        _disk['controllerKey'] = dev.controllerKey
                        _disk['unitNumber'] = dev.unitNumber
                        _disk['wwid'] = 'SCSI(%d:%d)' % (
                                dev.controllerKey - 1000, dev.unitNumber)
                        _disk['thinProvisioned'] = dev.backing.thinProvisioned
                        vm['base_disks'].append(_disk)

                    total_capacity_in_kb += dev.capacityInKB
                    if dev.backing.thinProvisioned:
                        total_thin_capacity_in_kb += dev.capacityInKB
                    else:
                        total_thick_capacity_in_kb += dev.capacityInKB

            vm['total_capacity_in_kb'] = total_capacity_in_kb
            vm['total_capacity'] = '%.2f TB' % (
                total_capacity_in_kb / 2.0 ** (10 * 3))

            vm['total_thin_capacity_in_kb'] = total_thin_capacity_in_kb
            vm['total_thick_capacity_in_kb'] = total_thick_capacity_in_kb

            self.vms.append(vm)

    @property
    def all_vms(self):
        return self.vms

    @property
    def total_vm_number(self):
        return len(self.vms)

    @property
    def total_disk_capacity(self):
        total_capacity_in_kb = 0L
        for vm in self.vms:
            total_capacity_in_kb += vm['total_capacity_in_kb']

        return '%.2f TB' % (
                total_capacity_in_kb / 2.0 ** (10 * 3))

    @property
    def total_thin_disk_capacity(self):
        total_thin_capacity_in_kb = 0L
        for vm in self.vms:
            total_thin_capacity_in_kb += vm['total_thin_capacity_in_kb']

        return '%.2f TB' % (
                total_thin_capacity_in_kb / 2.0 ** (10 * 3))

    @property
    def total_thick_disk_capacity(self):
        total_thick_capacity_in_kb = 0L
        for vm in self.vms:
            total_thick_capacity_in_kb += vm['total_thick_capacity_in_kb']

        return '%.2f TB' % (
                total_thick_capacity_in_kb / 2.0 ** (10 * 3))

    @property
    def total_datastore_capacity(self):
        total_ds_capacity = 0L
        self._all_dss()  # To fix vim.fault.NotAuthenticated issue
        for ds in self.dss:
            total_ds_capacity += ds.summary.capacity

        return '%.2f TB' % (
            total_ds_capacity / 2.0 ** (10 * 4))

    @property
    def total_left_datastore_capacity(self):
        total_left_ds_capacity = 0L
        self._all_dss()  # To fix vim.fault.NotAuthenticated issue
        for ds in self.dss:
            total_left_ds_capacity += ds.summary.freeSpace

        return '%.2f TB' % (
            total_left_ds_capacity / 2.0 ** (10 * 4))

    @property
    def total_provision_datastore_capacity(self):
        total_provision_ds_capacity = 0L
        self._all_dss()  # To fix vim.fault.NotAuthenticated issue
        for ds in self.dss:
                total_provision_ds_capacity += ds.summary.uncommitted
                total_provision_ds_capacity += ds.summary.freeSpace

        return '%.2f TB' % (
            total_provision_ds_capacity / 2.0 ** (10 * 4))

    def __iter__(self):
        for vm in self.vms:
            yield vm

    def deploy(self, param, vcenter, user, password):
        param.dataStore = 'SC8000_LUN12_WAN_TW'
        folder = 'AS_Saipu_6.0/WangXi'
        deploy_cmd = """\
        ovftool \
        --acceptAllEulas \
        --datastore=\"%s\" -dm=thin \
        --name=%s \
        --net:\"VM Network\"=\"VM Network\" \
        --prop:va.hostname=%s \
        --prop:va.ip=%s \
        --prop:va.netmask=%s \
        --prop:va.gateway=%s \
        --prop:va.dnsDomain=%s \
        --prop:va.dns1=%s \
        --prop:va.adPoolName=%s \
        --prop:va.adStuName=%s \
        --prop:va.msdpPoolName=%s \
        --prop:va.msdpStuName=%s \
        --overwrite \
        --powerOffTarget \
        --noSSLVerify \
        %s \"vi://%s:%s@%s/%s/\"""" \
                     % (param.dataStore, param.hostName,
                        param.hostName, param.ipAddress, param.netmask,
                        param.gateway, param.dnsDomain, param.dnsServer,
                        param.adPoolName, param.adStuName,
                        param.msdpPoolName, param.msdpStuName,
                        param.ovaFilePath, user, password, vcenter, folder)
        print(deploy_cmd)
        return

def get_vcenter_prop(conf=None):
    if not conf:
        conf = join(BASE_CONF_DIR, 'vm.conf')
    return vcenter_conf(conf)


def vcenter_conf(conf):
    """Parse the vcenter configuration file"""
    if file_exists(conf):
        parser = configparser.SafeConfigParser()
        with open(conf, 'r') as f:
            parser.readfp(f)

        def get(parser, name):
            """Parse key-value from the conf file"""
            if parser.has_option('vcenter', name):
                return parser.get('vcenter', name)
            else:
                raise Exception(
                    'Unable to get value of \'%s\' on vcenter part' % name)

        return (get(parser, 'vcenter'), get(parser, 'user'),
                get(parser, 'password'))

    else:
        raise Exception('File %s doesn\'t exits' % conf)

